/*
Data structure to hold bore data for a 2D section.

@author: Armin Schmidt, A.Schmidt@GeodataWIZ.com
@version: 1.0
@date: 9 April 2012
@history: V1.0 - 09/04/2012 - Start

The methods and internal variables are encapsulated with the functional pattern 
(see D. Crockford (2008). JavaScript: The Good Parts. O'Reilly, Sebastopol CA. p. 52).

The data structure itself is simple: each bore is an element of a 1D array 
and is represented as an object with un-nested bore properties
(e.g. bore[0] = {label: 'Bore 1', linearDistance: 101.2, elevation: 234.5, ...}).

The individual bores are gathered in an internal store ('addBore', 'addBores') 
and when they are retrieved ('getBores', 'getProperties', 'asString') they are first
normalised by being sorted (by their 'linearDistance') and then it is ensured
that each bore has records confirming exactly with the 'boreDescriptor' object.

The 'boreDescriptor' is passed in when creating a variable of the 
functional pattern 'boreTransect' (i.e. var myTransect=boreTransect(dataDescriptor);).
The 'boreDescriptor' is an object of all the bore properties and each of them holds
several parameters indicated by a property key
(e.g. boreDescriptor={
 label: {defaultValue:'Bore ?', dataCategory:dataCategories.DESCRIPTOR},
 elevation: {zeroAllowed:false, attemptInterpolate:true},
 ..
}). 
While any property keys deemed useful can be added to the 'boreDescriptor' there are
some that will always be prefilled as defaults (but can of course be overwritten).
These default keys are specified in the internal constant 'defaultPropertyKeys'.
They are:
 'dataCategory': the category of the data. This makes it easier to process bore properties
  that aren't known before. The possible category values are stored in the 
  global object dataCategories (default is 'dataCategories.DESCRIPTOR').
 'zeroAllowed': determines whether a zero value is allowed (default is true).
 'attemptInterpolate': if no valid value is provided for the bore property, should it be 
  derived from its neighbouring bores (default is true)?
 'defaultValue': if no valid value can be found use this one (default is 0.0).

If the 'dataCategory' is 'dataCategories.DATE' then a conversion of strings 
to 'Date' objects is performed.
If 'boreDescriptor[key].dateFormat' is provided then parsing is done with
date.js by Matt Kruse: http://www.mattkruse.com/javascript/date/
Mismatches between input and format are not tolerated.
If no 'dateFormat' is given then JavaScript's 'Date.parse()' is used:
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
*/

/**
 * Core VVG libraries. Must load this file before app.js.
 * 
 * @author: King Lung Chiu
 */
(function(vvg){     
   var lib = vvg.lib = vvg.lib || {};
   
   var dataCategories = {
      DESCRIPTOR: 'DESCRIPTOR',
      INTERFACE: 'INTERFACE',
      RANGE: 'RANGE',
      MEASUREMENT: 'MEASUREMENT',
      DATE: 'DATE'
   };
   var defaultBoreDescriptor = {
      label: {
         dataCategory: dataCategories.DESCRIPTOR,
         attemptInterpolate: false,
         defaultValue: 'Bore ?'
      },
      linearDistance: {
         dataCategory: dataCategories.DESCRIPTOR
      },
      elevation: {
         dataCategory: dataCategories.DESCRIPTOR,
         zeroAllowed: false,
         defaultValue: 100.0
      },
      offset: {
         dataCategory: dataCategories.DESCRIPTOR
      },
      depth: {
         dataCategory: dataCategories.DESCRIPTOR
      },
      groundwaterLevel: {
         dataCategory: dataCategories.INTERFACE,
         interfaceIndex: 0,
         style: 'blue' // Or anything else that allows to style the level and the area below it.
      },
      groundwaterLevelDate: {
         dataCategory: dataCategories.DATE,
         relatesTo: 'groundwaterLevel',
         attemptInterpolate: false,
         zeroAllowed: false,
         defaultValue: '1970-01-01 00:00',
         dateFormat: 'yyyy-MM-dd H:mm'
         // If 'boreDescriptor[key].dateFormat' is provided, parsing is done with
         // date.js by Matt Kruse http://www.mattkruse.com/javascript/date/
         // Mismatches between input and format are not tolerated.
         // If no 'dateFormat' is given JavaScript's 'Date.parse()' is used.
         // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
      },
      eh: {  // This is Eh, the redox potential.
         dataCategory: dataCategories.MEASUREMENT
      }
   };
      
   lib.bore = {
      // Global object to identify the category of a bore property.
      dataCategories: dataCategories
      ,defaultBoreDescriptor: defaultBoreDescriptor
      
      ,boreTransect: function (boreDescriptor) {
         var that = {};  // The main data object to hold all public variables and methods.

         // Private variables.
         var bores = [],  // Holds the array of bores. Each bore is a flat object of bore properties. The 'isComplete' bore property is added when ingesting.
            isNormalised = false;  // If no change since the last call to 'normalise()' nothing needs to be done.
         // Private constants.
         var defaultPropertyKeys = {
            dataCategory: dataCategories.DESCRIPTOR,
            defaultValue: 0.0,
            zeroAllowed: true,
            attemptInterpolate: true
         };

         // Private functions to work on the 'boreDescriptor'.
         var fillMissingPropertyKeys = function (key) {
            // In the 'boreDescriptor' each bore property, represented by its 'key',
            // gets certain 'propertyKeys' set to default values,
            // if they are not already defined.
            var propertyKey;
            for (propertyKey in defaultPropertyKeys) {
               if (defaultPropertyKeys.hasOwnProperty(propertyKey)) {
                  // The value of the 'propertyKey' could be boolean, so the test for existence has to be robust.
                  if (key[propertyKey] === undefined) {
                     key[propertyKey] = defaultPropertyKeys[propertyKey];
                  }
               }
            }
         };

         // Private functions to work on data in the bores.
         var getValueFromBore = function (bore, key) {
            // Return the value for the given bore property, or undefined.
            // The bore property could be boolean or string.
            if (bore[key] !== undefined) { // The property is defined.
               // But if it is 0 and that's not allowed, it is deemed undefined.
               if (bore[key] === 0 && !boreDescriptor[key].zeroAllowed) {
                  return undefined;
               }
            }
            return bore[key];
         };
         var getValue = function (iBore, key) {
            return getValueFromBore(bores[iBore], key);
         };
         var sortBores = function () {
            // Sort the bores by 'linearDistance' to allow interpolation of other properties later.
            bores.sort(function (b1, b2) {
               var v1 = getValueFromBore(b1, 'linearDistance');
               var v2 = getValueFromBore(b2, 'linearDistance');
               if (v1 === undefined || v2 === undefined || v1 === v2) {
                  return 0;
               } else {
                  return v1 - v2;
               }
            });
         };
         var completeBores = function () {
            // Complete all bores so that no properties are ill-defined.
            // In particular, the properties listed in the 'boreDescriptor' 
            // have to be available.
            var completeBore = function (iBore) {
               // Complete the bore 'iBore' so that no properties are ill-defined.
               var interpolateProperty = function (iBore, key)  {
                  // Fill a missing property value from neighbours.
                  // @@ The current implementation is simplistic, just copying values from neighbours.
                  // @@ A real interpolation might be better.
                  var v;
                  if (iBore > 0) { // Is it not the first bore?
                     // Use the value from the previous bore, as that will already have been corrected.
                     bores[iBore][key] = getValue(iBore-1, key);
                  } else {
                     // If it is the first bore that has no value try to get it from the next, or use a default.
                     v = getValue(iBore+1, key);
                     if (v !== undefined) {
                        bores[iBore][key] = v;
                     } else {
                        bores[iBore][key] = boreDescriptor[key].defaultValue;
                     }
                  }
               };
               var key, v, numDate, date;
               for (key in boreDescriptor) {
                  if (boreDescriptor.hasOwnProperty(key)) {
                     v = getValue(iBore, key);
                     if (v === undefined) { // Is the value ill-defined?
                        if (boreDescriptor[key].attemptInterpolate) { // Should there be an attempt to derive the value from neighbours?
                           interpolateProperty(iBore, key);
                        } else {
                           // No interpolation, so use a default value.
                           bores[iBore][key] = boreDescriptor[key].defaultValue;
                        }
                        v = bores[iBore][key];
                     }
                     if (boreDescriptor[key].dataCategory === dataCategories.DATE) {
                        // Convert all string date representations to 'Date' objects.
                        if (typeof v === 'string') {
                           if (boreDescriptor[key].dateFormat) {
                              numDate = getDateFromFormat(v, boreDescriptor[key].dateFormat);
                           } else {
                              numDate = Date.parse(v);
                           }
                           // In both cases, parsing errors result in 'numDate === 0'.
                           date = new Date();
                           if (numDate) {
                              date.setTime(numDate);
                           } else {
                              date.setTime(Date.parse(boreDescriptor[key].defaultValue));
                           }
                           bores[iBore][key] = date;
                        }
                     }
                  }
               }
               bores[iBore].isComplete = true;
            };
            var iBore;
            for (iBore = 0; iBore < bores.length; iBore += 1) {
               if (!bores[iBore].isComplete) {  // If the bore has not yet been completed.
                  completeBore(iBore);
               }
            }
         };
         var normalise = function () {
            // Normalise all bores so they are sorted and complete.
            if (!isNormalised) {
               sortBores();
               completeBores();
               isNormalised = true;
            }
         };

         // Privileged methods and variables, exposed outside.
         that.addBore = function (bore) {
            // Add the given bore.
            bore.isComplete = false;  // Add this extra property as state parameter.
            isNormalised = false;  // No longer sorted and probably this one incomplete.
            bores.push(bore);
         };
         that.addBores = function (bores) {
            // Add all bores from the given array.
            var i;
            for (i = 0; i < bores.length; i += 1) {
               that.addBore(bores[i]);
            }
         };
         that.getBores = function () {
            normalise();
            return bores;
         };
         that.getProperties = function () {
            // Return an object where each property is an array of one bore property.
            var obj = {};
            var iBore, key;
            normalise();
            for (key in boreDescriptor) {
               if (boreDescriptor.hasOwnProperty(key)) {
                  obj[key] = [];
                  for (iBore = 0; iBore < bores.length; iBore += 1) {
                     obj[key].push(getValue(iBore, key));
                  }
               }
            }
            return obj;
         };
         that.asString = function () {
            var s = '';
            var iBore, key;
            normalise();
            for (iBore = 0; iBore < bores.length; iBore += 1) {
               s += '{';
               for (key in boreDescriptor) {
                  if (boreDescriptor.hasOwnProperty(key)) {
                     s += key + ': ' + getValue(iBore, key) + ', ';
                  }
               }
               s += '}\n';
            }
            return s;
         };

         // Fill missing property keys in the 'boreDescriptor'.
         var key;
         for (key in boreDescriptor) {
            if (boreDescriptor.hasOwnProperty(key)) {
               fillMissingPropertyKeys(boreDescriptor[key]);
            }
         }
         return that;
      }
   };
      

}(window.vvg = window.vvg || {}));


